package com.arong.swing.db;

import org.droid.java.beans.BeanInfo;
import org.droid.java.beans.IntrospectionException;
import org.droid.java.beans.Introspector;
import org.droid.java.beans.PropertyDescriptor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Blob;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.BeanProcessor;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

/**
 * 由于要对propertyDescriptors方法进行重载，
 * 但<code>BeanProcessor</code>类中本方法为private，因此将该类方法中的全部
 * 内容进行了复制。重点重新了<code>propertyDescriptors</code>方法和
 * <code>mapColumnsToProperties</code>方法。
 */
public class OracleEntityBeanProcessor extends BeanProcessor {
    /**
     * 过滤掉类属性中的class属性
     */
    private static final String propClass = "class";
    
    /**
     * 重写<code>BeanProcessor</code>类的方法，方法用于获得Eitity类
     * 的属性信息
     * @param c 要获取属性值的类
     * @return 属性信息数组
     * @throws SQLException 数据库操作异常（实际上是将IntrospectionException转化为SQLException）
     */
    protected static PropertyDescriptor[] propertyDescriptors(Class<?> c) throws SQLException {
        BeanInfo beanInfo = null;
        try {
            beanInfo = Introspector.getBeanInfo(c);
        }
        catch(IntrospectionException e) {
            throw new SQLException("获取实体Bean的属性信息失败（" + c.getName() + "）: " + e.getMessage());
        }

        PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
        for(int i = 0; i < props.length; i++) {
            if(StringUtils.equals(props[i].getName(), propClass)) {
                props = (PropertyDescriptor[])ArrayUtils.remove(props, i);
                break;
            }
        }

        return props;
    }

    /**
     * 重载<code>BeanProcessor</code>类的方法，主要将数据表和数据库表实体字段的
     * 映射关系的算法进行了调整，将字段中的下滑线（“_”）删除后，再与字段名称进行是否
     * 相同的判定。
     * @param rsmd  结果集元数据对象
     * @param props 实体表属性对象数组
     * @return 字段与属性映射关系的数组
     * @throws SQLException 数据库操作异常
     */
    protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
                                           PropertyDescriptor[] props) throws SQLException {
        int cols = rsmd.getColumnCount();
        int[] columnToProperty = new int[cols + 1];
        Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

        for(int col = 1; col <= cols; col++) {
            String columnName = rsmd.getColumnLabel(col);
            if(null == columnName || 0 == columnName.length()) {
                columnName = rsmd.getColumnName(col);
            }
            for(int i = 0; i < props.length; i++) {
                columnName = StringUtils.remove(columnName, '_'); //去掉数据库字段中的下滑线
                if(columnName.equalsIgnoreCase(props[i].getName())) {
                    columnToProperty[col] = i;
                    break;
                }
            }
        }

        return columnToProperty;
    }

    /**
     * Special array value used by <code>mapColumnsToProperties</code> that
     * indicates there is no bean property that matches a column from a
     * <code>ResultSet</code>.
     */
    protected static final int PROPERTY_NOT_FOUND = -1;

    /**
     * Set a bean's primitive properties to these defaults when SQL NULL
     * is returned.  These are the same as the defaults that ResultSet get*
     * methods return in the event of a NULL column.
     */
    private static final Map<Class<?>, Object> primitiveDefaults = new HashMap<Class<?>, Object>();

    static {
        primitiveDefaults.put(Integer.TYPE, Integer.valueOf(0));
        primitiveDefaults.put(Short.TYPE, Short.valueOf((short)0));
        primitiveDefaults.put(Byte.TYPE, Byte.valueOf((byte)0));
        primitiveDefaults.put(Float.TYPE, Float.valueOf(0f));
        primitiveDefaults.put(Double.TYPE, Double.valueOf(0d));
        primitiveDefaults.put(Long.TYPE, Long.valueOf(0L));
        primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
        primitiveDefaults.put(Character.TYPE, Character.valueOf((char)0));
    }

    /**
     * Constructor for EntityBeanProcessor.
     */
    public OracleEntityBeanProcessor() {
        super();
    }

    /**
     * Convert a <code>ResultSet</code> row into a JavaBean.  This
     * implementation uses reflection and <code>BeanInfo</code> classes to
     * match column names to bean property names.  Properties are matched to
     * columns based on several factors:
     * <br/>
     * <ol>
     * <li>
     * The class has a writable property with the same name as a column.
     * The name comparison is case insensitive.
     * </li>
     * <p/>
     * <li>
     * The column type can be converted to the property's set method
     * parameter type with a ResultSet.get* method.  If the conversion fails
     * (ie. the property was an int and the column was a Timestamp) an
     * SQLException is thrown.
     * </li>
     * </ol>
     * <p/>
     * <p>
     * Primitive bean properties are set to their defaults when SQL NULL is
     * returned from the <code>ResultSet</code>.  Numeric fields are set to 0
     * and booleans are set to false.  Object bean properties are set to
     * <code>null</code> when SQL NULL is returned.  This is the same behavior
     * as the <code>ResultSet</code> get* methods.
     * </p>
     *
     * @param <T>  The type of bean to create
     * @param rs   ResultSet that supplies the bean data
     * @param type Class from which to create the bean instance
     * @return the newly created bean
     * @throws SQLException if a database access error occurs
     */
    public <T> T toBean(ResultSet rs, Class<T> type) throws SQLException {
        PropertyDescriptor[] props = propertyDescriptors(type);

        ResultSetMetaData rsmd = rs.getMetaData();
        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

        return this.createBean(rs, type, props, columnToProperty);
    }

    /**
     * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans.
     * This implementation uses reflection and <code>BeanInfo</code> classes to
     * match column names to bean property names. Properties are matched to
     * columns based on several factors:
     * <br/>
     * <ol>
     * <li>
     * The class has a writable property with the same name as a column.
     * The name comparison is case insensitive.
     * </li>
     * <p/>
     * <li>
     * The column type can be converted to the property's set method
     * parameter type with a ResultSet.get* method.  If the conversion fails
     * (ie. the property was an int and the column was a Timestamp) an
     * SQLException is thrown.
     * </li>
     * </ol>
     * <p/>
     * <p>
     * Primitive bean properties are set to their defaults when SQL NULL is
     * returned from the <code>ResultSet</code>.  Numeric fields are set to 0
     * and booleans are set to false.  Object bean properties are set to
     * <code>null</code> when SQL NULL is returned.  This is the same behavior
     * as the <code>ResultSet</code> get* methods.
     * </p>
     *
     * @param <T>  The type of bean to create
     * @param rs   ResultSet that supplies the bean data
     * @param type Class from which to create the bean instance
     * @return the newly created List of beans
     * @throws SQLException if a database access error occurs
     */
    public <T> List<T> toBeanList(ResultSet rs, Class<T> type) throws SQLException {
        List<T> results = new ArrayList<T>();

        if(!rs.next()) {
            return results;
        }

        PropertyDescriptor[] props = propertyDescriptors(type);
        ResultSetMetaData rsmd = rs.getMetaData();
        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

        do {
            results.add(this.createBean(rs, type, props, columnToProperty));
        }
        while(rs.next());

        return results;
    }

    /**
     * Creates a new object and initializes its fields from the ResultSet.
     *
     * @param <T>              The type of bean to create
     * @param rs               The result set.
     * @param type             The bean type (the return type of the object).
     * @param props            The property descriptors.
     * @param columnToProperty The column indices in the result set.
     * @return An initialized object.
     * @throws SQLException if a database error occurs.
     */
    private <T> T createBean(ResultSet rs, Class<T> type,
                             PropertyDescriptor[] props, int[] columnToProperty)
            throws SQLException {
        T bean = this.newInstance(type);

        for(int i = 1; i < columnToProperty.length; i++) {
            if(columnToProperty[i] == PROPERTY_NOT_FOUND) {
                continue;
            }

            PropertyDescriptor prop = props[columnToProperty[i]];
            Class<?> propType = prop.getPropertyType();

            Object value = this.processColumn(rs, i, propType);

            if(propType != null && value == null && propType.isPrimitive()) {
                value = primitiveDefaults.get(propType);
            }

            this.callSetter(bean, prop, value);
        }

        return bean;
    }

    /**
     * Calls the setter method on the target object for the given property.
     * If no setter method exists for the property, this method does nothing.
     *
     * @param target The object to set the property on.
     * @param prop   The property to set.
     * @param value  The value to pass into the setter.
     * @throws SQLException if an error occurs setting the property.
     */
    private void callSetter(Object target, PropertyDescriptor prop, Object value)
            throws SQLException {

        Method setter = prop.getWriteMethod();

        if(setter == null) {
            return;
        }

        Class<?>[] params = setter.getParameterTypes();
        try {
            // convert types for some popular ones
            if(value != null) {
                if(value instanceof java.util.Date) {
                    if(params[0].getName().equals("java.sql.Date")) {
                        value = new java.sql.Date(((java.util.Date)value).getTime());
                    } else if(params[0].getName().equals("java.sql.Time")) {
                        value = new java.sql.Time(((java.util.Date)value).getTime());
                    } else if(params[0].getName().equals("java.sql.Timestamp")) {
                        value = new java.sql.Timestamp(((java.util.Date)value).getTime());
                    }
                }
            }

            // Don't call setter if the value object isn't the right type
            if(this.isCompatibleType(value, params[0])) {
                setter.invoke(target, new Object[]{value});
            } else {
                throw new SQLException(
                        "Cannot set " + prop.getName() + ": incompatible types.");
            }
        }
        catch(IllegalArgumentException e) {
            throw new SQLException(
                    "Cannot set " + prop.getName() + ": " + e.getMessage());
        }
        catch(IllegalAccessException e) {
            throw new SQLException(
                    "Cannot set " + prop.getName() + ": " + e.getMessage());
        }
        catch(InvocationTargetException e) {
            throw new SQLException(
                    "Cannot set " + prop.getName() + ": " + e.getMessage());
        }
    }

    /**
     * ResultSet.getObject() returns an Integer object for an INT column.  The
     * setter method for the property might take an Integer or a primitive int.
     * This method returns true if the value can be successfully passed into
     * the setter method.  Remember, Method.invoke() handles the unwrapping
     * of Integer into an int.
     *
     * @param value The value to be passed into the setter method.
     * @param type  The setter's parameter type.
     * @return boolean True if the value is compatible.
     */
    private boolean isCompatibleType(Object value, Class<?> type) {
        // Do object check first, then primitives
        if(value == null || type.isInstance(value)) {
            return true;

        } else if(type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
            return true;
        } else if(type.equals(Long.TYPE) && Long.class.isInstance(value)) {
            return true;
        } else if(type.equals(Double.TYPE) && Double.class.isInstance(value)) {
            return true;
        } else if(type.equals(Float.TYPE) && Float.class.isInstance(value)) {
            return true;
        } else if(type.equals(Short.TYPE) && Short.class.isInstance(value)) {
            return true;
        } else if(type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
            return true;
        } else if(type.equals(Character.TYPE) && Character.class.isInstance(value)) {
            return true;
        } else if(type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
            return true;
        }
        return false;
    }

    /**
     * Factory method that returns a new instance of the given Class.  This
     * is called at the start of the bean creation process and may be
     * overridden to provide custom behavior like returning a cached bean
     * instance.
     *
     * @param <T> The type of object to create
     * @param c   The Class to create an object from.
     * @return A newly created object of the Class.
     * @throws SQLException if creation failed.
     */
    protected <T> T newInstance(Class<T> c) throws SQLException {
        try {
            return c.newInstance();
        }
        catch(InstantiationException e) {
            throw new SQLException(
                    "Cannot create " + c.getName() + ": " + e.getMessage());
        }
        catch(IllegalAccessException e) {
            throw new SQLException(
                    "Cannot create " + c.getName() + ": " + e.getMessage());
        }
    }

    /**
     * Convert a <code>ResultSet</code> column into an object.  Simple
     * implementations could just call <code>rs.getObject(index)</code> while
     * more complex implementations could perform type manipulation to match
     * the column's type to the bean property type.
     * <p/>
     * <p>
     * This implementation calls the appropriate <code>ResultSet</code> getter
     * method for the given property type to perform the type conversion.  If
     * the property type doesn't match one of the supported
     * <code>ResultSet</code> types, <code>getObject</code> is called.
     * </p>
     *
     * @param rs       The <code>ResultSet</code> currently being processed.  It is
     *                 positioned on a valid row before being passed into this method.
     * @param index    The current column index being processed.
     * @param propType The bean property type that this column needs to be
     *                 converted into.
     * @return The object from the <code>ResultSet</code> at the given column
     *         index after optional type processing or <code>null</code> if the column
     *         value was SQL NULL.
     * @throws SQLException if a database access error occurs
     */
    protected Object processColumn(ResultSet rs, int index, Class<?> propType)
            throws SQLException {
        if(!propType.isPrimitive() && rs.getObject(index) == null) {
            return null;
        }

        if(propType.equals(String.class)) {
            return rs.getString(index);
        } else if(
                propType.equals(Integer.TYPE) || propType.equals(Integer.class)) {
            return Integer.valueOf(rs.getInt(index));
        } else if(
                propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) {
            return Boolean.valueOf(rs.getBoolean(index));
        } else if(propType.equals(Long.TYPE) || propType.equals(Long.class)) {
            return Long.valueOf(rs.getLong(index));
        } else if(
                propType.equals(Double.TYPE) || propType.equals(Double.class)) {
            return Double.valueOf(rs.getDouble(index));
        } else if(
                propType.equals(Float.TYPE) || propType.equals(Float.class)) {
            return Float.valueOf(rs.getFloat(index));
        } else if(
                propType.equals(Short.TYPE) || propType.equals(Short.class)) {
            return Short.valueOf(rs.getShort(index));
        } else if(propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
            return Byte.valueOf(rs.getByte(index));
        } else if(propType.equals(Timestamp.class)) {
            return rs.getTimestamp(index);
        }
        else  if(propType.equals(byte[].class)) {
            try {
                Blob blob = rs.getBlob(index);
                InputStream is = blob.getBinaryStream();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int b = is.read();
                while(b != -1){
                    baos.write(b);
                    b = is.read();
                }
                byte[] content = baos.toByteArray();

                is.close();
                return content;
            	//return rs.getBytes(index);
            }catch(Exception e) {
                throw new SQLException("获取Blob类型字段时发生错误：" + e.getMessage());
            }
        } else {
            return rs.getObject(index);
        }
    }
}